package org.tabcap.workspace.document;

import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.undo.*;

/*
**  This class will merge individual edits into a single larger edit.
**  That is, characters entered sequentially will be grouped together and
**  undone as a group. Any attribute changes will be considered as part
**  of the group and will therefore be undone when the group is undone.
*
** SRC: Online, at:
**            http://tips4java.wordpress.com/2008/10/27/compound-undo-manager/
*/
public class CompoundUndoManager extends UndoManager
		implements UndoableEditListener, DocumentListener
{
	private static final long serialVersionUID = 724866257343353033L;
	
	private UndoManager undoManager;
	private CompoundEdit compoundEdit;
	private JTextComponent textComponent;
	private UndoAction undoAction;
	private RedoAction redoAction;

	//  These fields are used to help determine whether the edit is an
	//  incremental edit. The offset and length should increase by 1 for
	//  each character added or decrease by 1 for each character removed.

	private int lastOffset;
	private int lastLength;

	public CompoundUndoManager(JTextComponent textComponent)
	{
		this.textComponent = textComponent;
		undoManager = this;
		undoAction = new UndoAction();
		redoAction = new RedoAction();
		textComponent.getDocument().addUndoableEditListener( this );
	}

	/*
	**  Add a DocumentLister before the undo is done so we can position
	**  the Caret correctly as each edit is undone.
	*/
	public void undo()
	{
		textComponent.getDocument().addDocumentListener( this );
		super.undo();
		textComponent.getDocument().removeDocumentListener( this );
	}

	/*
	**  Add a DocumentLister before the redo is done so we can position
	**  the Caret correctly as each edit is redone.
	*/
	public void redo()
	{
		textComponent.getDocument().addDocumentListener( this );
		super.redo();
		textComponent.getDocument().removeDocumentListener( this );
	}

	/*
	**  Whenever an UndoableEdit happens the edit will either be absorbed
	**  by the current compound edit or a new compound edit will be started
	*/
	public void undoableEditHappened(UndoableEditEvent e)
	{
		//  Start a new compound edit

		if (compoundEdit == null)
		{
			compoundEdit = startCompoundEdit( e.getEdit() );
			return;
		}

		int offsetChange = textComponent.getCaretPosition() - lastOffset;
		int lengthChange = textComponent.getDocument().getLength() - lastLength;

		//  Check for an attribute change

		AbstractDocument.DefaultDocumentEvent event =
			(AbstractDocument.DefaultDocumentEvent)e.getEdit();

		if  (event.getType().equals(DocumentEvent.EventType.CHANGE))
		{
			if (offsetChange == 0)
			{
				compoundEdit.addEdit(e.getEdit() );
				return;
			}
		}

		//  Check for an incremental edit or backspace.
		//  The Change in Caret position and Document length should both be
		//  either 1 or -1.

		if (offsetChange == lengthChange &&  Math.abs(offsetChange) == 1)
		{
			compoundEdit.addEdit( e.getEdit() );
			lastOffset = textComponent.getCaretPosition();
			lastLength = textComponent.getDocument().getLength();
			return;
		}

		//  Not incremental edit, end previous edit and start a new one

		compoundEdit.end();
		compoundEdit = startCompoundEdit( e.getEdit() );
	}

	/*
	**  Each CompoundEdit will store a group of related incremental edits
	**  (ie. each character typed or backspaced is an incremental edit)
	*/
	private CompoundEdit startCompoundEdit(UndoableEdit anEdit)
	{
		//  Track Caret and Document information of this compound edit

		lastOffset = textComponent.getCaretPosition();
		lastLength = textComponent.getDocument().getLength();

		//  The compound edit is used to store incremental edits

		compoundEdit = new MyCompoundEdit();
		compoundEdit.addEdit( anEdit );

		//  The compound edit is added to the UndoManager. All incremental
		//  edits stored in the compound edit will be undone/redone at once

		addEdit( compoundEdit );

		undoAction.updateUndoState();
		redoAction.updateRedoState();

		return compoundEdit;
	}

	/*
	 *  The Action to Undo changes to the Document.
	 *  The state of the Action is managed by the CompoundUndoManager
	 */
	public Action getUndoAction()
	{
		return undoAction;
	}

	/*
	 *  The Action to Redo changes to the Document.
	 *  The state of the Action is managed by the CompoundUndoManager
	 */
	public Action getRedoAction()
	{
		return redoAction;
	}
//
//  Implement DocumentListener
//
	/*
	 *  Updates to the Document as a result of Undo/Redo will cause the
	 *  Caret to be repositioned
	 */
	public void insertUpdate(final DocumentEvent e)
	{
		SwingUtilities.invokeLater(new Runnable()
		{
			public void run()
			{
				int offset = e.getOffset() + e.getLength();
				offset = Math.min(offset, textComponent.getDocument().getLength());
				textComponent.setCaretPosition( offset );
			}
		});
	}

	public void removeUpdate(DocumentEvent e)
	{
		textComponent.setCaretPosition(e.getOffset());
	}

	public void changedUpdate(DocumentEvent e) {}


	class MyCompoundEdit extends CompoundEdit
	{
		private static final long serialVersionUID = 7487569133432931737L;

		public boolean isInProgress()
		{
			//  in order for the canUndo() and canRedo() methods to work
			//  assume that the compound edit is never in progress

			return false;
		}

		public void undo() throws CannotUndoException
		{
			//  End the edit so future edits don't get absorbed by this edit

			if (compoundEdit != null)
				compoundEdit.end();

			super.undo();

			//  Always start a new compound edit after an undo

			compoundEdit = null;
		}
	}

	/*
	 *	Perform the Undo and update the state of the undo/redo Actions
	 */
	class UndoAction extends AbstractAction
	{
		private static final long serialVersionUID = -1470567214706279155L;

		public UndoAction()
		{
			putValue( Action.NAME, "Undo" );
			putValue( Action.SHORT_DESCRIPTION, getValue(Action.NAME) );
			putValue( Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_U) );
			putValue( Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control Z") );
			setEnabled(false);
		}

		public void actionPerformed(ActionEvent e)
		{
			try
			{
				undoManager.undo();
				textComponent.requestFocusInWindow();
			}
			catch (CannotUndoException ex) {}

			updateUndoState();
			redoAction.updateRedoState();
		}

		private void updateUndoState()
		{
			setEnabled( undoManager.canUndo() );
		}
	}

	/*
	 *	Perform the Redo and update the state of the undo/redo Actions
	 */
	class RedoAction extends AbstractAction
	{
		private static final long serialVersionUID = 9176823918170560749L;

		public RedoAction()
		{
			putValue( Action.NAME, "Redo" );
			putValue( Action.SHORT_DESCRIPTION, getValue(Action.NAME) );
			putValue( Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_R) );
			putValue( Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK) );
			setEnabled(false);
		}

		public void actionPerformed(ActionEvent e)
		{
			try
			{
				undoManager.redo();
				textComponent.requestFocusInWindow();
			}
			catch (CannotRedoException ex) {}

			updateRedoState();
			undoAction.updateUndoState();
		}

		protected void updateRedoState()
		{
			setEnabled( undoManager.canRedo() );
		}
	}
}
